﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using System.Xml.XPath;

namespace Xml2Yaml
{
    class XPathParser
    {
        private const string YamlTag = @"__YAML__TAG___";
        private Stack<string> CurrentPath = new Stack<string>();
        private Stack<YamlNode> XNodes = new Stack<YamlNode>();
        private Dictionary<string, YamlNode> CacheNodes = new Dictionary<string, YamlNode>();
        private IDictionary<string, string> NamespaceMap;
        private XPathNavigator CacheNavigator = null;
        private Dictionary<string, XPathNodeIterator> CacheSets = new Dictionary<string, XPathNodeIterator>();
        private IXmlNamespaceResolver CacheNamespaceResolver;
        private XDocument CacheXmlDocument;
        private string CurrentAttribute;
        private string CurrentValue;

        public XPathParser(IDictionary<string, string> namespaces)
        {
            NamespaceMap = namespaces;
        }

        private void Clear()
        {
            CacheNavigator = null;
            CurrentAttribute = string.Empty;
            CurrentValue = string.Empty;
        }

        public void Push(string ns, string localName, string text, IDictionary<AttributeType, string> attr = null)
        {
            Clear();

            if (string.IsNullOrEmpty(ns))
            {
                CurrentPath.Push(localName);
            }
            else
            {
                string nsabbr;
                if (NamespaceMap.TryGetValue(ns, out nsabbr))
                {
                    CurrentPath.Push(nsabbr + ":" + localName);
                }
                else
                {
                    CurrentPath.Push(localName);
                }
            }

            string xpath = CurrentXPath;
            YamlNode node;
            if (CacheNodes.TryGetValue(xpath, out node))
            {
                node.Reference(text, attr);
            }
            else
            {
                node = new YamlNode(ns, localName, text, attr);
                CacheNodes.Add(xpath, node);
            }

            XNodes.Push(node);
        }

        public bool Pop()
        {
            Clear();

            if (XNodes.Count > 0)
            {
                CurrentPath.Pop();

                YamlNode node = XNodes.Pop();
                if (node.Attributes != null)
                {
                    node.Attributes.Clear();
                }

                node.Text = string.Empty;

                return true;
            }

            return false;
        }

        private bool Peek(string text)
        {
            YamlNode node = PeekNode;
            if (node != null)
            {
                node.Text = CurrentValue = YamlUtil.Trim(text);
                if (!string.IsNullOrEmpty(CurrentValue)
                   && string.IsNullOrEmpty(CurrentAttribute)
                   && CacheXmlDocument != null) //only handle on node
                {
                    XElement element = CacheXmlDocument.XPathSelectElement(CompileFullXPath, CacheNamespaceResolver);
                    if (element != null)
                    {
                        element.SetValue(CurrentValue);
                    }
                }
                return true;
            }
            return false;
        }

        private YamlNode PeekNode
        {
            get
            {
                return XNodes.Count == 0 ? null : XNodes.Peek();
            }
        }

        public XPathParser Attach(string attribute)
        {
            if (!string.IsNullOrEmpty(attribute))
            {
                CurrentAttribute = attribute;
            }
            return this;
        }

        public XPathParser Attach(StringBuilder text)
        {
            Peek(text.ToString());
            return this;
        }

        private XPathNavigator XDocumentTree
        {
            get
            {
                if (CacheNavigator == null)
                {
                    CacheXmlDocument = BuildXDocument(NamespaceMap);
                    CacheNavigator = CacheXmlDocument == null ? null : CacheXmlDocument.CreateNavigator();
                    if (CacheNavigator != null)
                    {
                        CacheNamespaceResolver = GetNamespaceResolver(CacheNavigator);
                    }
                }

                return CacheNavigator;
            }
        }

        private XDocument BuildXDocument(IDictionary<string, string> nsMap)
        {
            XDocument doc = null;

            if (XNodes.Count > 0)
            {
                CacheSets.Clear();

                if (YamlUtil.IsWindowsPlatForm())
                {
                    doc = new XDocument();
                    using (XmlWriter xw = doc.CreateWriter())
                    {
                        WriteDocument(xw, nsMap);
                    }
                }
                else  //MONO
                {
                    doc = XDocument.Parse(BuildXmlTree(nsMap).ToString());
                }
            }

            return doc;
        }

        private void WriteDocument(XmlWriter writer, IDictionary<string, string> nsMap)
        {
            writer.WriteStartDocument();

            int index = 0;
            foreach (var node in XNodes.Reverse())
            {
                ++index;

                string localName = node.LocalName;

                for (int j = node.Count; j > 1; --j)
                {
                    if (string.IsNullOrEmpty(node.Namespace))
                    {
                        writer.WriteElementString(localName, string.Empty);
                    }
                    else
                    {
                        writer.WriteElementString(localName, node.Namespace, string.Empty);
                    }
                }

                //start Element
                writer.WriteStartElement(localName, node.Namespace);
                if (node.Attributes != null)
                {
                    foreach (var pair in node.Attributes)
                    {
                        //TODO: fixed namespace in attribute
                        localName = pair.Key.LocalName;
                        if (!string.IsNullOrEmpty(localName)
                            && !localName.Contains("&")
                            && !localName.Contains("*")
                            )
                        {
                            localName = RemoveAttributePrefix(localName);
                            writer.WriteAttributeString(localName, pair.Value);
                        }
                    }
                }

                if (index == XNodes.Count)
                {
                    writer.WriteAttributeString(YamlTag, YamlTag);
                    writer.WriteString(node.Text);
                    writer.WriteEndElement();
                }

            }

            for (int i = index - 1; i > 0; --i)
            {
                writer.WriteEndElement();
            }

            writer.WriteEndDocument();
        }

        private string RemoveAttributePrefix(string attr)
        {
            if (attr.StartsWith("@", StringComparison.OrdinalIgnoreCase))
            {
                return attr.Substring(1);
            }
            return attr;
        }

        /// <summary>
        /// Simple format, such as /root/a/b
        /// </summary>
        private string CurrentXPath
        {
            get
            {
                IEnumerable<string> ele = CurrentPath.Reverse<string>();
                string xpath = "/" + string.Join("/", ele);
                if (!string.IsNullOrEmpty(CurrentAttribute))
                {
                    xpath += "/@" + CurrentAttribute;
                }
                return xpath;
            }
        }

        /// <summary>
        /// detail format: such as /root[1]/a[2]/b[3]
        /// </summary>
        /// <param name="detail"></param>
        /// <returns></returns>
        private string CompileFullXPath
        {
            get
            {
                StringBuilder sb = new StringBuilder();
                foreach (var node in XNodes.Reverse())
                {
                    sb.Append("/");
                    if (!string.IsNullOrEmpty(node.Namespace))
                    {
                        string ns;
                        if (NamespaceMap.TryGetValue(node.Namespace, out ns))
                        {
                            sb.Append(ns);
                            sb.Append(":");
                        }
                    }
                    sb.Append(node.LocalName);
                    sb.Append("[");
                    sb.Append(node.Count);
                    sb.Append("]");
                    if (!string.IsNullOrEmpty(CurrentAttribute))
                    {
                        sb.Append("/@" + CurrentAttribute);
                    }
                }
                return sb.ToString();
            }
        }

        private string ParseLocalName(string xpath)
        {
            int pos = xpath.LastIndexOf('/');
            if (pos > -1 && pos < xpath.Length - 1)
            {
                return xpath.Substring(pos + 1);
            }
            return xpath;
        }

        private IXmlNamespaceResolver GetNamespaceResolver(XPathNavigator navigator)
        {
            if (navigator != null)
            {
                XmlNamespaceManager namespaceManager = new XmlNamespaceManager(navigator.NameTable);
                foreach (var pair in NamespaceMap)
                {
                    namespaceManager.AddNamespace(pair.Value, pair.Key);
                }
                return namespaceManager;
            }

            return null;
        }

        public bool QueryWithXPath(IEnumerable<string> list)
        {
            string xpath = CurrentXPath;
            string localName = ParseLocalName(xpath);

            return list.Contains(xpath)
                || list.Contains(localName)
                || ExistsInTree(list);
        }

        private bool ExistsInTree(IEnumerable<string> list)
        {
            XPathNavigator root = XDocumentTree;
            if (root != null)
            {
                foreach (string xpath in list)
                {
                    XPathNodeIterator itr;
                    if (!CacheSets.TryGetValue(xpath, out itr))
                    {
                        itr = root.Select(xpath, CacheNamespaceResolver);
                        CacheSets.Add(xpath, itr);
                    }

                    if (itr != null)
                    {
                        foreach (XPathNavigator item in itr)
                        {
                            if (!string.IsNullOrEmpty(CurrentAttribute)) //compare attribute 
                            {
                                if (item.Name == CurrentAttribute)
                                {
                                    return true;
                                }
                            }
                            else if (XPathNavigatorUtil.GetAttribute(item, YamlTag) == YamlTag
                                    || (xpath.EndsWith("*", StringComparison.InvariantCultureIgnoreCase)
                                        && item.OuterXml.Contains(YamlTag)))
                            {
                                return true;
                            }
                        }
                    }
                }
            }
            return false;
        }

        public string QueryWithXPath(IDictionary<string, string> map, string localName)
        {
            string xpath = CurrentXPath;
            //  string localName = ParseLocalName(xpath);

            string result;
            if (!map.TryGetValue(localName, out result)
                && !map.TryGetValue(xpath, out result)
                )
            {

                XPathNavigator root = XDocumentTree;
                if (root != null)
                {
                    foreach (var pair in map)
                    {
                        xpath = pair.Key;

                        XPathNodeIterator itr;
                        if (!CacheSets.TryGetValue(xpath, out itr))
                        {
                            itr = root.Select(xpath, CacheNamespaceResolver);
                            CacheSets.Add(xpath, itr);
                        }

                        if (itr != null)
                        {
                            foreach (XPathNavigator item in itr)
                            {
                                if (!string.IsNullOrEmpty(CurrentAttribute)) //compare attribute 
                                {
                                    if (item.Name == CurrentAttribute)
                                    {
                                        return pair.Value;
                                    }
                                }
                                else if (XPathNavigatorUtil.GetAttribute(item, YamlTag) == YamlTag
                                    && item.LocalName == localName)
                                {
                                    return pair.Value;
                                }
                            }
                        }
                    }
                }

                if (string.IsNullOrEmpty(result))
                {
                    result = RemoveAttributePrefix(localName);
                }
            }

            return result;
        }

        #region Only For Debug Test

        public override string ToString()
        {
#if DEBUG
            return BuildXmlTree(NamespaceMap).ToString();
#else
            return base.ToString();
#endif
        }

        #endregion

        /// <summary>
        /// 
        /// </summary>
        /// <param name="nsMap"></param>
        /// <returns></returns>
        private StringBuilder BuildXmlTree(IDictionary<string, string> nsMap)
        {
            StringBuilder sb = new StringBuilder();
            if (XNodes.Count > 0)
            {
                XmlWriterSettings xws = new XmlWriterSettings();
                xws.Indent = true;
                using (XmlWriter xw = XmlWriter.Create(sb, xws))
                {
                    WriteDocument(xw, nsMap);
                }
            }

            //because Mono can not support UTF-16 header string,
            //
            return sb.Replace("<?xml version=\"1.0\" encoding=\"utf-16\"?>",
                    "<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
                .Replace("<?xml version=\"1.0\" encoding=\"UTF-16\"?>",
                "<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
            
        }


       
    }
}
